Mestre JavaScript Promise-kombinatorer (Promise.all, Promise.allSettled, Promise.race, Promise.any) for effektiv og robust asynkron programmering i globale applikasjoner.
JavaScript Promise-kombinatorer: Avanserte asynkrone mønstre for globale applikasjoner
Asynkron programmering er en hjørnestein i moderne JavaScript, spesielt når man bygger nettapplikasjoner som samhandler med API-er, databaser eller utfører tidkrevende operasjoner. JavaScript Promises gir en kraftig abstraksjon for å håndtere asynkrone operasjoner, men for å mestre dem kreves det forståelse av avanserte mønstre. Denne artikkelen dykker ned i JavaScript Promise-kombinatorer – Promise.all, Promise.allSettled, Promise.race og Promise.any – og hvordan de kan brukes til å skape effektive og robuste asynkrone arbeidsflyter, spesielt i konteksten av globale applikasjoner med varierende nettverksforhold og datakilder.
Forståelse av Promises: En rask oppsummering
Før vi dykker inn i kombinatorer, la oss raskt gjennomgå Promises. Et Promise representerer det endelige resultatet av en asynkron operasjon. Det kan være i en av tre tilstander:
- Avventende (Pending): Den opprinnelige tilstanden, verken oppfylt eller avvist.
- Oppfylt (Fulfilled): Operasjonen ble fullført vellykket, med en resulterende verdi.
- Avvist (Rejected): Operasjonen mislyktes, med en årsak (vanligvis et Error-objekt).
Promises tilbyr en renere og mer håndterbar måte å håndtere asynkrone operasjoner på sammenlignet med tradisjonelle callbacks. De forbedrer kodens lesbarhet og forenkler feilhåndtering. Avgjørende er at de også danner grunnlaget for Promise-kombinatorene vi skal utforske.
Promise-kombinatorer: Orkestrering av asynkrone operasjoner
Promise-kombinatorer er statiske metoder på Promise-objektet som lar deg administrere og koordinere flere Promises. De gir kraftige verktøy for å bygge komplekse asynkrone arbeidsflyter. La oss undersøke hver enkelt i detalj.
Promise.all(): Utføre Promises parallelt og samle resultater
Promise.all() tar imot en iterable (vanligvis en array) av Promises som input og returnerer et enkelt Promise. Dette returnerte Promiset oppfylles når alle input-Promises er oppfylt. Hvis noen av input-Promisene avvises, avvises det returnerte Promiset umiddelbart med årsaken til det første avviste Promiset.
Brukstilfelle: Når du trenger å hente data fra flere API-er samtidig og behandle de kombinerte resultatene, er Promise.all() ideelt. Tenk deg for eksempel at du bygger et dashbord som viser værinformasjon fra forskjellige byer rundt om i verden. Dataene for hver by kan hentes via et separat API-kall.
async function fetchWeatherData(city) {
try {
const response = await fetch(`https://api.example.com/weather?city=${city}`); // Erstatt med et ekte API-endepunkt
if (!response.ok) {
throw new Error(`Klarte ikke å hente værdata for ${city}`);
}
return await response.json();
} catch (error) {
console.error(`Feil ved henting av værdata for ${city}: ${error}`);
throw error; // Kast feilen videre slik at den fanges av Promise.all
}
}
async function displayWeatherData() {
const cities = ['London', 'Tokyo', 'New York', 'Sydney'];
try {
const weatherDataPromises = cities.map(city => fetchWeatherData(city));
const weatherData = await Promise.all(weatherDataPromises);
weatherData.forEach((data, index) => {
console.log(`Vær i ${cities[index]}:`, data);
// Oppdater brukergrensesnittet med værdataene
});
} catch (error) {
console.error('Klarte ikke å hente værdata for alle byene:', error);
// Vis en feilmelding til brukeren
}
}
displayWeatherData();
Vurderinger for globale applikasjoner:
- Nettverksforsinkelse: Forespørsler til forskjellige API-er på forskjellige geografiske steder kan oppleve varierende forsinkelse.
Promise.all()garanterer ikke rekkefølgen Promisene oppfylles i, bare at de alle oppfylles (eller ett avvises) før det kombinerte Promiset blir avgjort. - API-ratebegrensning: Hvis du gjør flere forespørsler til samme API eller flere API-er med delte ratebegrensninger, kan du overskride disse grensene. Implementer strategier som å sette forespørsler i kø eller bruke eksponentiell backoff for å håndtere ratebegrensning på en elegant måte.
- Feilhåndtering: Husk at hvis noen Promise avvises, mislykkes hele
Promise.all()-operasjonen. Dette er kanskje ikke ønskelig hvis du vil vise delvise data selv om noen forespørsler mislykkes. Vurder å brukePromise.allSettled()i slike tilfeller (forklart nedenfor).
Promise.allSettled(): Håndtere suksess og feil individuelt
Promise.allSettled() ligner på Promise.all(), men med en avgjørende forskjell: det venter på at alle input-Promises skal bli avgjort, uavhengig av om de oppfylles eller avvises. Det returnerte Promiset oppfylles alltid med en array av objekter, der hvert objekt beskriver utfallet av det tilsvarende input-Promiset. Hvert objekt har en status-egenskap (enten "fulfilled" eller "rejected") og en value (hvis oppfylt) eller reason (hvis avvist) egenskap.
Brukstilfelle: Når du trenger å samle resultater fra flere asynkrone operasjoner, og det er akseptabelt at noen mislykkes uten at hele operasjonen feiler, er Promise.allSettled() det bedre valget. Tenk deg et system som behandler betalinger gjennom flere betalingsgatewayer. Du vil kanskje forsøke alle betalinger og registrere hvilke som lyktes og hvilke som mislyktes.
async function processPayment(paymentGateway, amount) {
try {
const response = await paymentGateway.process(amount); // Erstatt med en ekte betalingsgateway-integrasjon
if (response.status === 'success') {
return { status: 'fulfilled', value: `Betaling behandlet vellykket via ${paymentGateway.name}` };
} else {
throw new Error(`Betaling mislyktes via ${paymentGateway.name}: ${response.message}`);
}
} catch (error) {
return { status: 'rejected', reason: `Betaling mislyktes via ${paymentGateway.name}: ${error.message}` };
}
}
async function processMultiplePayments(paymentGateways, amount) {
const paymentPromises = paymentGateways.map(gateway => processPayment(gateway, amount));
const results = await Promise.allSettled(paymentPromises);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(result.value);
} else {
console.error(result.reason);
}
});
// Analyser resultatene for å avgjøre samlet suksess/feil
const successfulPayments = results.filter(result => result.status === 'fulfilled').length;
const failedPayments = results.filter(result => result.status === 'rejected').length;
console.log(`Vellykkede betalinger: ${successfulPayments}`);
console.log(`Mislykkede betalinger: ${failedPayments}`);
}
// Eksempel på betalingsgatewayer
const paymentGateways = [
{ name: 'PayPal', process: (amount) => Promise.resolve({ status: 'success', message: 'Betaling vellykket' }) },
{ name: 'Stripe', process: (amount) => Promise.reject({ status: 'error', message: 'Ikke nok midler' }) },
{ name: 'Worldpay', process: (amount) => Promise.resolve({ status: 'success', message: 'Betaling vellykket' }) },
];
processMultiplePayments(paymentGateways, 100);
Vurderinger for globale applikasjoner:
- Robusthet:
Promise.allSettled()øker robustheten til applikasjonene dine ved å sikre at alle asynkrone operasjoner blir forsøkt, selv om noen mislykkes. Dette er spesielt viktig i distribuerte systemer der feil er vanlige. - Detaljert rapportering: Resultat-arrayen gir detaljert informasjon om hver operasjons utfall, slik at du kan logge feil, prøve mislykkede operasjoner på nytt, eller gi brukere spesifikk tilbakemelding.
- Delvis suksess: Du kan enkelt bestemme den samlede suksessraten og iverksette passende tiltak basert på antall vellykkede og mislykkede operasjoner. For eksempel kan du tilby alternative betalingsmetoder hvis den primære gatewayen mislykkes.
Promise.race(): Velge det raskeste resultatet
Promise.race() tar også imot en iterable av Promises som input og returnerer et enkelt Promise. Men i motsetning til Promise.all() og Promise.allSettled(), blir Promise.race() avgjort så snart noen av input-Promisene blir avgjort (enten oppfylt eller avvist). Det returnerte Promiset oppfylles eller avvises med verdien eller årsaken til det første avgjorte Promiset.
Brukstilfelle: Når du trenger å velge den raskeste responsen fra flere kilder, er Promise.race() et godt valg. Tenk deg å spørre flere servere om de samme dataene og bruke den første responsen du mottar. Dette kan forbedre ytelse og respons, spesielt i situasjoner der noen servere kan være midlertidig utilgjengelige eller tregere enn andre.
async function fetchDataFromServer(serverURL) {
try {
const response = await fetch(serverURL, {signal: AbortSignal.timeout(5000)}); //Legg til en tidsavbrudd på 5 sekunder
if (!response.ok) {
throw new Error(`Klarte ikke å hente data fra ${serverURL}`);
}
return await response.json();
} catch (error) {
console.error(`Feil ved henting av data fra ${serverURL}: ${error}`);
throw error;
}
}
async function getFastestResponse() {
const serverURLs = [
'https://server1.example.com/data', // Erstatt med ekte server-URL-er
'https://server2.example.com/data',
'https://server3.example.com/data',
];
try {
const dataPromises = serverURLs.map(serverURL => fetchDataFromServer(serverURL));
const fastestData = await Promise.race(dataPromises);
console.log('Raskeste data mottatt:', fastestData);
// Bruk de raskeste dataene
} catch (error) {
console.error('Klarte ikke å hente data fra noen server:', error);
// Håndter feilen
}
}
getFastestResponse();
Vurderinger for globale applikasjoner:
- Tidsavbrudd: Det er avgjørende å implementere tidsavbrudd når du bruker
Promise.race()for å forhindre at det returnerte Promiset venter i uendelighet hvis noen av input-Promisene aldri blir avgjort. Eksempelet over bruker `AbortSignal.timeout()` for å oppnå dette. - Nettverksforhold: Den raskeste serveren kan variere avhengig av brukerens geografiske plassering og nettverksforhold. Vurder å bruke et Content Delivery Network (CDN) for å distribuere innholdet ditt og forbedre ytelsen for brukere over hele verden.
- Feilhåndtering: Hvis den Promisen som 'vinner' kappløpet avvises, blir hele Promise.race avvist. Sørg for at hver Promise har passende feilhåndtering for å forhindre uventede avvisninger. Også, hvis den "vinnende" promisen avvises på grunn av tidsavbrudd (som vist ovenfor), vil de andre promisene fortsette å kjøre i bakgrunnen. Du må kanskje legge til logikk for å avbryte de andre promisene med `AbortController` hvis de ikke lenger er nødvendige.
Promise.any(): Akseptere den første oppfyllelsen
Promise.any() ligner på Promise.race(), men med en litt annen oppførsel. Det venter på at det første input-Promiset skal oppfylles. Hvis alle input-Promises avvises, avvises Promise.any() med en AggregateError som inneholder en array av avvisningsårsakene.
Brukstilfelle: Når du trenger å hente data fra flere kilder, og du bare bryr deg om det første vellykkede resultatet, er Promise.any() et godt valg. Dette er nyttig når du har redundante datakilder eller alternative API-er som gir den samme informasjonen. Det prioriterer suksess over hastighet, da det venter på den første oppfyllelsen, selv om noen Promises avvises raskt.
async function fetchDataFromSource(sourceURL) {
try {
const response = await fetch(sourceURL);
if (!response.ok) {
throw new Error(`Klarte ikke å hente data fra ${sourceURL}`);
}
return await response.json();
} catch (error) {
console.error(`Feil ved henting av data fra ${sourceURL}: ${error}`);
throw error;
}
}
async function getFirstSuccessfulData() {
const dataSources = [
'https://source1.example.com/data', // Erstatt med ekte datakilde-URL-er
'https://source2.example.com/data',
'https://source3.example.com/data',
];
try {
const dataPromises = dataSources.map(sourceURL => fetchDataFromSource(sourceURL));
const data = await Promise.any(dataPromises);
console.log('Første vellykkede data mottatt:', data);
// Bruk de vellykkede dataene
} catch (error) {
if (error instanceof AggregateError) {
console.error('Klarte ikke å hente data fra noen kilde:', error.errors);
// Håndter feilen
} else {
console.error('En uventet feil oppstod:', error);
}
}
}
getFirstSuccessfulData();
Vurderinger for globale applikasjoner:
- Redundans:
Promise.any()er spesielt nyttig når man håndterer redundante datakilder som gir lignende informasjon. Hvis en kilde er utilgjengelig eller treg, kan du stole på at de andre gir dataene. - Feilhåndtering: Sørg for å håndtere
AggregateErrorsom kastes når alle input-Promises avvises. Denne feilen inneholder en array av de individuelle avvisningsårsakene, slik at du kan feilsøke og diagnostisere problemene. - Prioritering: Rekkefølgen du gir Promises til
Promise.any()har betydning. Plasser de mest pålitelige eller raskeste datakildene først for å øke sannsynligheten for et vellykket resultat.
Velge riktig kombinator: En oppsummering
Her er en rask oppsummering for å hjelpe deg med å velge den passende Promise-kombinatoren for dine behov:
- Promise.all(): Brukes når du trenger at alle Promises oppfylles vellykket, og du vil feile umiddelbart hvis noen Promise avvises.
- Promise.allSettled(): Brukes når du vil vente på at alle Promises blir avgjort, uavhengig av suksess eller feil, og du trenger detaljert informasjon om hvert utfall.
- Promise.race(): Brukes når du vil velge det raskeste resultatet fra flere Promises, og du bare bryr deg om den første som blir avgjort.
- Promise.any(): Brukes når du vil akseptere det første vellykkede resultatet fra flere Promises, og du ikke har noe imot at noen Promises avvises.
Avanserte mønstre og beste praksis
Utover den grunnleggende bruken av Promise-kombinatorer, er det flere avanserte mønstre og beste praksis å huske på:
Begrense samtidighet
Når du håndterer et stort antall Promises, kan det å utføre dem alle parallelt overvelde systemet ditt eller overskride API-ratebegrensninger. Du kan begrense samtidighet ved hjelp av teknikker som:
- Oppdeling (Chunking): Del opp Promises i mindre biter og behandle hver bit sekvensielt.
- Bruke en semafor: Implementer en semafor for å kontrollere antall samtidige operasjoner.
Her er et eksempel som bruker oppdeling:
async function processInChunks(promises, chunkSize) {
const results = [];
for (let i = 0; i < promises.length; i += chunkSize) {
const chunk = promises.slice(i, i + chunkSize);
const chunkResults = await Promise.all(chunk);
results.push(...chunkResults);
}
return results;
}
// Eksempelbruk
const myPromises = [...Array(100)].map((_, i) => Promise.resolve(i)); //Opprett 100 promises
processInChunks(myPromises, 10) // Prosesser 10 promises om gangen
.then(results => console.log('Alle promises er løst:', results));
Håndtere feil på en elegant måte
Korrekt feilhåndtering er avgjørende når du jobber med Promises. Bruk try...catch-blokker for å fange feil som kan oppstå under asynkrone operasjoner. Vurder å bruke biblioteker som p-retry eller retry for å automatisk prøve mislykkede operasjoner på nytt.
async function fetchDataWithRetry(url, retries = 3) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP-feil! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (retries > 0) {
console.log(`Prøver på nytt om 1 sekund... (Forsøk igjen: ${retries})`);
await new Promise(resolve => setTimeout(resolve, 1000)); // Vent 1 sekund
return fetchDataWithRetry(url, retries - 1);
} else {
console.error('Maks antall forsøk nådd. Operasjonen mislyktes.');
throw error;
}
}
}
Bruke Async/Await
async og await gir en mer synkron-lignende måte å jobbe med Promises på. De kan betydelig forbedre kodens lesbarhet og vedlikeholdbarhet.
Husk å bruke try...catch-blokker rundt await-uttrykk for å håndtere potensielle feil.
Avbrytelse (Cancellation)
I noen scenarier kan det hende du må avbryte ventende Promises, spesielt når du håndterer langvarige operasjoner eller brukerinitierte handlinger. Du kan bruke AbortController-API-et for å signalisere at et Promise skal avbrytes.
const controller = new AbortController();
const signal = controller.signal;
async function fetchDataWithCancellation(url) {
try {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP-feil! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Henting avbrutt');
} else {
console.error('Feil ved henting av data:', error);
}
throw error;
}
}
fetchDataWithCancellation('https://api.example.com/data')
.then(data => console.log('Data mottatt:', data))
.catch(error => console.error('Henting mislyktes:', error));
// Avbryt henteoperasjonen etter 5 sekunder
setTimeout(() => {
controller.abort();
}, 5000);
Konklusjon
JavaScript Promise-kombinatorer er kraftige verktøy for å bygge robuste og effektive asynkrone applikasjoner. Ved å forstå nyansene i Promise.all, Promise.allSettled, Promise.race og Promise.any, kan du orkestrere komplekse asynkrone arbeidsflyter, håndtere feil på en elegant måte og optimalisere ytelsen. Når du utvikler globale applikasjoner, er det avgjørende å vurdere nettverksforsinkelse, API-ratebegrensninger og påliteligheten til datakilder. Ved å anvende mønstrene og beste praksis som er diskutert i denne artikkelen, kan du lage JavaScript-applikasjoner som er både ytelsessterke og motstandsdyktige, og som leverer en overlegen brukeropplevelse til brukere over hele verden.